﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;
using NetFrameworkExtensions;
using System.Collections;
using System.ComponentModel.DataAnnotations;

namespace SoftwareEngineering.Modeling
{
  [Serializable]
  public abstract class ModelBase: IValidatableObject
  {
    public ModelBase Root 
    { 
      get 
      { 
        return GetAncestorsAndSelf().Last(); 
      } 
    }

    public IEnumerable<ModelBase> GetAncestorsAndSelf()
    {
      return this.Unfold(m => m.Parent, m => m == null);
    }

    [field: NonSerialized]
    public ModelBase Parent { get; set; }

    public TModel GetParentAs<TModel>() where TModel : ModelBase
    {
      return Parent as TModel;
    }

    public TModel GetClosest<TModel>() where TModel : ModelBase
    {
      return GetAncestorsAndSelf().Skip(1).OfType<TModel>().FirstOrDefault();
    }

    [field: NonSerialized]
    public PropertyInfo ParentProperty { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext context)
    {
      foreach (var error in ValidateOverride(context))
        yield return error;
      foreach (var tup in GetRelatedWithPropertyFunc(this))
      {
        var subContext = context.CreateSubContext(tup.Item1, tup.Item2);
        var errors = new List<ValidationResult>();
        if (!tup.Item2.TryValidateInternal(subContext, errors))
          foreach (var error in errors)
            yield return error;
      }
    }

    /// <summary>
    /// Validate local data in the model, and data which depends on current model only
    /// </summary>
    /// <remarks>
    /// Global validations like cycles and multi-model validations must be done in 
    /// the container of all the models, instead on each individual model possibly 
    /// involved in the global validation
    /// </remarks>
    /// <param name="context"></param>
    /// <returns></returns>
    protected virtual IEnumerable<ValidationResult> ValidateOverride(ValidationContext context)
    {
      yield break;
    }

    internal void Initialize(IServiceProvider services)
    {
      this.DepthFirstSearch(GetRelatedFunc, true).ForEach(model => model.InitializeReferences(services));
    }

    private static IEnumerable<ModelBase> GetRelatedFunc(ModelBase model)
    {
      var lookup = model.GetChildren();
      foreach (var contentProperty in lookup)
      {
        var property = contentProperty.Key;
        foreach (var child in contentProperty)
        {
          child.Parent = model;
          child.ParentProperty = property;
        }
      }
      return lookup.SelectMany(g => g);
    }

    private static IEnumerable<Tup<PropertyInfo, ModelBase>> GetRelatedWithPropertyFunc(ModelBase model)
    {
      var lookup = model.GetChildren();
      foreach (var contentProperty in lookup)
      {
        var property = contentProperty.Key;
        foreach (var child in contentProperty)
        {
          child.Parent = model;
          child.ParentProperty = property;
        }
      }
      return lookup.SelectMany(g => g.Select(c => Tup.Create(g.Key, c)));
    }

    protected virtual void InitializeReferences(IServiceProvider services)
    {

    }

    protected virtual ILookup<PropertyInfo, ModelBase> GetChildren()
    {
      var result = new LookupImpl<PropertyInfo, ModelBase>();
      var properties = GetType()
        .GetProperties()
        .Where(p => p.GetGetMethod() != null)
        .Where(p => p.GetCustomAttributes(typeof(ContentPropertyAttribute), true).Any())
        .ToList();
      foreach (var property in properties)
      {
        var value = property.GetValue(this, null);
        if (value is ModelBase)
        {
          result.Groupings.Add(property, new GroupingImpl<PropertyInfo, ModelBase>()
          {
            Key = property,
            Elements = Seq.Build((ModelBase)value).ToList()
          });
        }
        else
          if (value is IEnumerable<ModelBase>)
          {
            result.Groupings.Add(property, new GroupingImpl<PropertyInfo, ModelBase>()
            {
              Key = property,
              Elements = ((IEnumerable<ModelBase>)value).ToList()
            });
          }
          else
            if (value != null)
              throw new InvalidOperationException(string.Format("Property {0} on Type {1} is marked as ContentProperty but its value is not ModelBase or IEnumerable<ModelBase>", property.Name, GetType().FullName));
      }
      return result;
    }

    public override string ToString()
    {
      return string.Format("[{0}]", GetType().Name);
    }
  }
}
